/*
 * This file is part of ELCube.
 *
 * ELCube is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ELCube is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with ELCube.  If not, see <https://www.gnu.org/licenses/>.
 */
package cn.nkpro.elcube.docengine.service.impl;

import cn.nkpro.elcube.data.elasticearch.*;
import cn.nkpro.elcube.data.redis.RedisSupport;
import cn.nkpro.elcube.docengine.NkDocProperties;
import cn.nkpro.elcube.docengine.NkDocSearchService;
import cn.nkpro.elcube.docengine.gen.DocDefIndexRuleMapper;
import cn.nkpro.elcube.docengine.model.SearchParams;
import cn.nkpro.elcube.docengine.model.es.AbstractBaseES;
import cn.nkpro.elcube.docengine.model.es.DocExtES;
import cn.nkpro.elcube.docengine.model.es.DocHES;
import cn.nkpro.elcube.docengine.service.NkDocPermService;
import cn.nkpro.elcube.task.model.BpmTaskES;
import cn.nkpro.elcube.utils.UUIDHexGenerator;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.xssf.usermodel.*;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.BucketOrder;
import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.filter.ParsedFilter;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestBuilders;
import org.elasticsearch.search.suggest.SuggestionBuilder;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder;
import org.springframework.beans.factory.annotation.Autowired;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

@Slf4j
public class NkDocSearchServiceImpl implements NkDocSearchService {

    @Autowired@SuppressWarnings("all")
    private SearchEngine searchEngine;
    @Autowired@SuppressWarnings("all")
    protected NkDocPermService docPermService;
    @Autowired@SuppressWarnings("all")
    private NkDocProperties docProperties;
    @Autowired@SuppressWarnings("all")
    private RedisSupport<List<Map<String, Object>>> redisList;
    @Autowired@SuppressWarnings("all")
    private RedisSupport<SearchParams> redisParam;
    @Autowired@SuppressWarnings("all")
    private DocDefIndexRuleMapper docDefIndexRuleMapper;

    @Override
    public void init() throws IOException {

        searchEngine.createIndices(DocHES.class);
        searchEngine.createIndices(BpmTaskES.class);
        searchEngine.createIndices(DocExtES.class);

        if(docProperties.getIndices()!=null){
            for(Map.Entry<String, Class<AbstractBaseES>> e : docProperties.getIndices().entrySet()){
                searchEngine.createIndices(e.getValue(),e.getKey());
            }
        }

        createDefaultRecord();
    }

    @Override
    public void createDefaultRecord(){

        List<String> keys = docDefIndexRuleMapper.selectByExample(null)
                .stream()
                .map(r -> r.getIndexName() + '_' + r.getIndexType())
                .distinct()
                .collect(Collectors.toList());

        searchEngine.indexBeforeCommit(
                new DocHES().makeDefaults(keys),
                new BpmTaskES().makeDefaults(keys),
                new DocExtES().makeDefaults(keys)
        );

        if(docProperties.getIndices()!=null){
            for(Map.Entry<String, Class<AbstractBaseES>> e : docProperties.getIndices().entrySet()){
                try {
                    searchEngine.indexBeforeCommit(e.getKey(), e.getValue().getConstructor().newInstance().makeDefaults(keys));
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    @Override
    public void dropAndInit() throws IOException {

        searchEngine.deleteIndices(DocHES.class);
        searchEngine.deleteIndices(BpmTaskES.class);
        searchEngine.deleteIndices(DocExtES.class);

        this.init();
    }

    @Override
    public boolean exists(String docId) throws IOException {
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.termQuery("docId",docId));
        return searchEngine.exists(DocHES.class,sourceBuilder);
    }


    @Override
    public ESPageList<JSONObject> queryList(
            String indexName,
            String preQueryFilter,
            SearchParams params
    ){
        return this.queryList(StringUtils.defaultIfBlank(indexName,"document"), preQueryFilter, params, false);
    }


    @Override
    public ESPageList<JSONObject> querySuggest(
            String indexName,
            String preQueryFilter,
            SearchParams params){


        BoolQueryBuilder postQueryBuilder = QueryBuilders.boolQuery();

        if(preQueryFilter!=null){
            postQueryBuilder.must(QueryBuilders.wrapperQuery(preQueryFilter));
        }
        // 功能前置条件
        if(params.getPostCondition()!=null) {
            postQueryBuilder.must(
                new LimitQueryBuilder(params.getPostCondition())
            );
        }

        QueryBuilder permQuery = docPermService.buildDocFilter(NkDocPermService.MODE_READ, null, null, false);
        if(permQuery!=null)
            postQueryBuilder.must(permQuery);

        // 构造检索语句
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
                .from(0)
                .size(0);
        // 过滤权限
        if(!postQueryBuilder.must().isEmpty())
            sourceBuilder.postFilter(postQueryBuilder);

        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        if(params.getConditions()!=null){
            JSONObject filter = params.getConditions();
            if(filter!=null){
                filter.forEach((k,v)-> boolQueryBuilder.must(new LimitQueryBuilder(filter.getJSONObject(k))));
            }
        }

        if(!boolQueryBuilder.must().isEmpty())
            sourceBuilder.query(boolQueryBuilder);

        sourceBuilder.fetchSource(params.getSuggest().getString("field"),null);

        SuggestionBuilder<CompletionSuggestionBuilder> suggestionBuilder = SuggestBuilders
                .completionSuggestion(
                        params.getSuggest().getString("field")
                )
                .prefix(
                        params.getSuggest().getString("text")
                );
        SuggestBuilder suggestBuilder = new SuggestBuilder();
        suggestBuilder.addSuggestion(params.getSuggest().getString("field"), suggestionBuilder);
        sourceBuilder.suggest(suggestBuilder);

        return searchPage(indexName,sourceBuilder);
    }

    @Override
    public byte[] download(String id){

        List<Map<String, Object>> list = redisList.get(id+"_data");
        SearchParams params = redisParam.get(id+"_params");

        redisList.delete(id+"_data",id+"_params");
        if(list==null || params==null){
            return null;
        }

        XSSFWorkbook workbook = new XSSFWorkbook();

        // 创建央视
        XSSFFont fontHeader = workbook.createFont();
        fontHeader.setBold(true);
        XSSFDataFormat format = workbook.createDataFormat();

        XSSFCellStyle cellStyleHeader = workbook.createCellStyle();
        cellStyleHeader.setFont(fontHeader);
        //cellStyleHeader.setAlignment(HorizontalAlignment.CENTER);
        cellStyleHeader.setVerticalAlignment(VerticalAlignment.CENTER);

        XSSFCellStyle cellStyleDate = workbook.createCellStyle();
        cellStyleDate.setDataFormat(format.getFormat("yyyy/m/d"));

        XSSFCellStyle cellStyleDateTime = workbook.createCellStyle();
        cellStyleDateTime.setDataFormat(format.getFormat("yyyy/m/d h:mm:ss"));

        XSSFCellStyle cellStyleCurrency = workbook.createCellStyle();
        cellStyleCurrency.setDataFormat(format.getFormat("¥#,##0.00"));

        XSSFCellStyle cellStylePercent = workbook.createCellStyle();
        cellStylePercent.setDataFormat(format.getFormat("0.0000%"));

        XSSFCellStyle cellStyleNumber = workbook.createCellStyle();
        cellStyleNumber.setDataFormat(format.getFormat("#0.00"));

        XSSFCellStyle cellStyleInteger = workbook.createCellStyle();
        cellStyleInteger.setDataFormat(format.getFormat("#0"));

        // 创建Sheet
        XSSFSheet sheet = workbook.createSheet("Sheet");
        XSSFRow head = sheet.createRow(0);

        List<SearchParams.Column> columns = params.getColumns()
                .stream()
                .filter(column -> StringUtils.isNotBlank(column.getField()))
                .collect(Collectors.toList());

        columns.forEach(column->{
            int index = columns.indexOf(column);
            XSSFCell cell = head.createCell(index);
            cell.setCellStyle(cellStyleHeader);
            cell.setCellValue(column.getTitle());
        });
        for(int i=0;i<list.size();i++){
            Map<String, Object> map = list.get(i);
            XSSFRow row = sheet.createRow(i+1);

            columns.forEach(column->{

                Object value=null;
                String field = column.getField();
                int point = field.indexOf(".");
                if(point>0){
                    @SuppressWarnings("all")
                    Map<String,Object> inner = (Map) map.get(field.substring(0,point));
                    if(inner!=null){
                        value = inner.get(field.substring(point+1));
                    }
                }else{
                    value = map.get(field);
                }

                int index = columns.indexOf(column);

                if(value!=null){
                    if(value instanceof Number){

                        XSSFCell cell = row.createCell(index, CellType.NUMERIC);

                        if(column.getExport()!=null){
                            String formatter = column.getExport().getFormatter();

                            if(StringUtils.equals(formatter,"date")){
                                row.createCell(index).setCellValue(new Date(((Number) value).longValue()*1000));
                                cell.setCellStyle(cellStyleDate);
                                return;
                            }else if(StringUtils.equals(formatter,"datetime")){
                                row.createCell(index).setCellValue(new Date(((Number) value).longValue()*1000));
                                cell.setCellStyle(cellStyleDateTime);
                                return;
                            }else if(StringUtils.equalsAnyIgnoreCase(formatter,"number","double","float")){
                                cell.setCellValue(((Number) value).doubleValue());
                                cell.setCellStyle(cellStyleNumber);
                                return;
                            }else if(StringUtils.equals(formatter,"percent")){
                                cell.setCellValue(((Number) value).doubleValue());
                                cell.setCellStyle(cellStylePercent);
                                return;
                            }else if(StringUtils.equalsAnyIgnoreCase(formatter,"integer","int")){
                                cell.setCellValue(((Number) value).intValue());
                                cell.setCellStyle(cellStyleInteger);
                                return;
                            }else if(StringUtils.equals(formatter,"currency")){
                                cell.setCellValue(((Number) value).doubleValue());
                                cell.setCellStyle(cellStyleCurrency);
                                return;
                            }
                        }else if(column.getFormatter()!=null){
                            CharSequence f = null;
                            if(column.getFormatter() instanceof List){
                                List formatter = (List) column.getFormatter();
                                if(!formatter.isEmpty()){
                                    f = (String) formatter.get(0);
                                }
                            }else if(column.getFormatter() instanceof String){
                                f = (String) column.getFormatter();
                            }

                            if(StringUtils.equalsAnyIgnoreCase(f,"nkDatetime")){
                                cell.setCellValue(new Date(((Number) value).longValue()*1000));
                                cell.setCellStyle(cellStyleDateTime);
                                return;
                            }else if(StringUtils.equalsAnyIgnoreCase(f,"nkDate")){
                                cell.setCellValue(new Date(((Number) value).longValue()*1000));
                                cell.setCellStyle(cellStyleDate);
                                return;
                            }else if(StringUtils.equalsAnyIgnoreCase(f,"nkDatetimeFriendly")){
                                cell.setCellValue(new Date(((Number) value).longValue()*1000));
                                cell.setCellStyle(cellStyleDateTime);
                                return;
                            }else if(StringUtils.equalsAnyIgnoreCase(f,"nkDatetimeISO")){
                                cell.setCellValue(new Date(((Number) value).longValue()));
                                cell.setCellStyle(cellStyleDateTime);
                                return;
                            }else if(StringUtils.equalsAnyIgnoreCase(f,"nkDateISO")){
                                cell.setCellValue(new Date(((Number) value).longValue()));
                                cell.setCellStyle(cellStyleDate);
                                return;
                            }else if(StringUtils.equalsAnyIgnoreCase(f,"nkMillisecondFriendly")){
                                cell.setCellValue(new Date(((Number) value).longValue()));
                                cell.setCellStyle(cellStyleDateTime);
                                return;
                            }else if(StringUtils.equalsAnyIgnoreCase(f,"nkCurrency")){
                                cell.setCellValue(((Number) value).doubleValue());
                                cell.setCellStyle(cellStyleCurrency);
                                return;
                            }else if(StringUtils.equalsAnyIgnoreCase(f,"nkPercent")){
                                cell.setCellValue(((Number) value).doubleValue());
                                cell.setCellStyle(cellStylePercent);
                                return;
                            }else if(StringUtils.equalsAnyIgnoreCase(f,"nkCurrency")){
                                cell.setCellValue(((Number) value).doubleValue());
                                cell.setCellStyle(cellStyleCurrency);
                                return;
                            }else if(StringUtils.equalsAnyIgnoreCase(f,"nkNumber","nkDouble","nkFloat")){
                                cell.setCellValue(((Number) value).doubleValue());
                                cell.setCellStyle(cellStyleNumber);
                                return;
                            }else if(StringUtils.equalsAnyIgnoreCase(f,"nkInteger","nkInt")){
                                cell.setCellValue(((Number) value).doubleValue());
                                cell.setCellStyle(cellStyleInteger);
                                return;
                            }
                        }

                        cell.setCellValue(((Number)value).doubleValue());
                    }else{
                        row.createCell(index).setCellValue(value.toString());
                    }
                }
            });
        }

        for(int i=0;i<columns.size();i++)
            sheet.autoSizeColumn(i);

        ByteArrayOutputStream fileOut;
        try {
            fileOut = new ByteArrayOutputStream();
            workbook.write(fileOut);
            fileOut.close();

            return fileOut.toByteArray();
        } catch (Exception e) {
            log.error(e.getMessage(),e);
            return null;
        }
    }

    @Override
    public String queryExport(
            String indexName,
            SearchParams params
    ) {
        params.setAggs(null);
        params.setSuggest(null);
        params.setHighlight(null);
        params.setFrom(0);
        params.setRows(10000);

        SearchSourceBuilder builder = createSourceBuilder(null,params,true);

        SearchResponse response = searchEngine.search(indexName, builder);


        SearchHit[] hits = response.getHits().getHits();

        String key = UUIDHexGenerator.generate();
        redisList.set(key+"_data",Arrays.stream(hits).map(SearchHit::getSourceAsMap).collect(Collectors.toList()));
        redisList.expire(key+"_data",60);

        redisParam.set(key+"_params",params);
        redisList.expire(key+"_params",60);

        return key;
    }

    @Override
    public ESPageList<JSONObject> queryList(
            String indexName,
            String preQueryFilter,
            SearchParams params,
            boolean filterByDocPerm
    ){
        return searchPage(indexName,createSourceBuilder(preQueryFilter,params,filterByDocPerm));
    }

    private SearchSourceBuilder createSourceBuilder(String preQueryFilter,
                   SearchParams params,
                   boolean filterByDocPerm){
        BoolQueryBuilder postQueryBuilder = QueryBuilders.boolQuery();

        // 默认过滤条件
        postQueryBuilder.must(new TermQueryBuilder("$", 1));

        if(preQueryFilter!=null){
            postQueryBuilder.must(QueryBuilders.wrapperQuery(preQueryFilter));
        }
        // 功能前置条件
        if(params.getPostCondition()!=null) {
            postQueryBuilder.must(
                    new LimitQueryBuilder(params.getPostCondition())
                    //QueryBuilders.wrapperQuery()
            );
        }

        if(filterByDocPerm){
            QueryBuilder permQuery = docPermService.buildDocFilter(NkDocPermService.MODE_READ, null, null, false);
            if(permQuery!=null)
                postQueryBuilder.must(permQuery);
        }

        // 构造检索语句
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
                .from(params.getFrom())
                .size(params.getRows());
        // 过滤权限
        if(!postQueryBuilder.must().isEmpty())
            sourceBuilder.postFilter(postQueryBuilder);

        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        if(params.getConditions()!=null){
            JSONObject filter = params.getConditions();
            if(filter!=null){
                filter.forEach((k,v)-> boolQueryBuilder.must(new LimitQueryBuilder(filter.getJSONObject(k))));
            }
        }

        if(!boolQueryBuilder.must().isEmpty())
            sourceBuilder.query(boolQueryBuilder);

        // 高亮
        List<String> highlightField = params.getHighlight();
        if(highlightField!=null && !highlightField.isEmpty()){
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            highlightBuilder.preTags("<span class='highlight'>");
            highlightBuilder.postTags("</span>");
            for(String field : highlightField){
                highlightBuilder.field(new HighlightBuilder.Field(field));
            }
            sourceBuilder.highlighter(highlightBuilder);
        }

        // 排序
        if(StringUtils.isNotBlank(params.getOrderField())){
            sourceBuilder.sort(SortBuilders
                    .fieldSort(params.getOrderField())
                    .order(SortOrder.fromString(StringUtils.defaultIfBlank(params.getOrder(),"desc")))
            );
            if(!StringUtils.equals(params.getOrderField(),"_score")){
                sourceBuilder.sort(SortBuilders.scoreSort());
            }
        }else{
            sourceBuilder.sort(SortBuilders.scoreSort());
            sourceBuilder.sort(SortBuilders.fieldSort("updatedTime").order(SortOrder.DESC));
        }

        // 过滤字段
        if(params.getSource()!=null){
            sourceBuilder.fetchSource(params.getSource(),null);
        }

        // 汇总数据
        if(params.getAggs()!=null) {
            FilterAggregationBuilder $aggs = AggregationBuilders
                    .filter("$aggs", postQueryBuilder);
            params.getAggs()
                    .forEach(agg ->
                            $aggs.subAggregation(
                                    AggregationBuilders.terms(agg)
                                            .field(agg)
                                            .order(BucketOrder.key(true))
                                            .size(100)
                            )
                    );
            sourceBuilder.aggregation($aggs);
        }

        return sourceBuilder;
    }

    private ESPageList<JSONObject> searchPage(String indexName, SearchSourceBuilder builder) {

        SearchResponse response = searchEngine.search(indexName, builder);

        List<JSONObject> collect = Arrays.stream(response.getHits().getHits())
                .map(hit -> {
                    JSONObject jsonObject = new JSONObject(hit.getSourceAsMap());

                    hit.getHighlightFields().forEach((k,v)-> {
                        if(v.getFragments()!=null && v.getFragments().length>0){
                            jsonObject.put(k,v.getFragments()[0].string());
                        }
                    });

                    return jsonObject;
                })
                .collect(Collectors.toList());

        List<Object> suggests = null;
        if(response.getSuggest()!=null){
            List<Object> sug = new ArrayList<>();
            response.getSuggest().forEach(i->{
                CompletionSuggestion completionSuggestion = (CompletionSuggestion) i;
                completionSuggestion.getOptions()
                        .forEach(option -> sug.add(option.getHit().getSourceAsMap()));
            });
            suggests = sug;
        }

        Map<String, ESAgg> aggs = null;
        if(response.getAggregations()!=null){

            Map<String, Aggregation> aggregationMap = response.getAggregations()
                    .asMap();

            ParsedFilter $aggs = (ParsedFilter) aggregationMap.get("$aggs");
            aggs = $aggs.getAggregations()
                    .asList()
                    .stream()
                    .map(aggregation -> {
                        ESAgg agg = new ESAgg();
                        ParsedStringTerms parsedStringTerms = (ParsedStringTerms) aggregation;
                        agg.setName(parsedStringTerms.getName());
                        agg.setBuckets(
                                parsedStringTerms.getBuckets()
                                        .stream()
                                        .map(bucket->{
                                            ESBucket bt = new ESBucket();
                                            bt.setKey(bucket.getKeyAsString());
                                            bt.setDocCount(bucket.getDocCount());
                                            return bt;
                                        })
                                        .collect(Collectors.toList())
                        );
                        return agg;
                    })
                    .collect(Collectors.toMap(ESAgg::getName, Function.identity()));
        }

        return new ESPageList<>(
                collect,
                aggs,
                suggests,
                builder.from(),
                builder.size(),
                response.getHits().getTotalHits().value
        );
    }
}
